/*
 * Copyright 2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package asphalt2.factory.impl
{
	import asphalt2.factory.BeanFactory;
	import asphalt2.factory.init;
	
	import flash.utils.describeType;
	import flash.utils.getQualifiedClassName;
	
	import mx.logging.ILogger;
	import mx.logging.Log;
	import mx.styles.CSSStyleDeclaration;
	import mx.styles.StyleManager;
	
	use namespace init;
	/**
	 * CSSで設定を行うBean生成Factory
	 * @auther c9katayama
	 */
	public class CSSBeanFactory implements BeanFactory
	{		
		private static const BEAN_REF_PATTEN:RegExp = /[$][{][^)]*[}]/;
		private static var logger:ILogger = Log.getLogger(getQualifiedClassName(CSSBeanFactory).replace("::","."));		
		
		private static const TYPE_CONFIG_NAME:String = "beanType";
		private static const SCOPE_CONFIG_NAME:String = "beanScope";
		private static const INIT_METHOD_CONFIG_NAME:String = "initMethod";		
		private static const CONFIG_NAME_LIST:Array = [TYPE_CONFIG_NAME,SCOPE_CONFIG_NAME,INIT_METHOD_CONFIG_NAME];

		private static var _defaultInstance:CSSBeanFactory;
		
		protected var _initialized:Boolean = false;		
		protected var _defaultBeanScope:String; 
		protected var _typeInfoCache:TypeInfoCache;
		protected var _beanScope:BeanScope;
		
		public function CSSBeanFactory(){
			_typeInfoCache = new TypeInfoCache();
			_beanScope = new BeanScope();
		}
		public static function getInstance():CSSBeanFactory{
			if(_defaultInstance==null){
				_defaultInstance = new CSSBeanFactory(); 
			}
			return _defaultInstance;
		}		
		protected function initialize():void{
			if(_defaultBeanScope==null){
				var css:CSSStyleDeclaration = findStyleDeclaration("CSSBeanFactory");
				var scope:String = BeanScope.SCOPE_SINGLETON
				if(css != null){
					var configScope:String = css.getStyle("defaultBeanScope");
					if(configScope != null){
						scope = configScope;
					}
				}
				defaultBeanScope = scope;
			}
			_initialized = true;
		}
		public function set defaultBeanScope(scope:String):void{
			checkScopeValue(scope);
			_defaultBeanScope = scope;
			logger.info("defaultBeanScope="+scope);
		}
		/**
		 * Classのショートネームを使用して、beanを検索し、beanインスタンスを返します。
		 */		
		public function getBeanByType(clazz:Class):*{
			var beanName:String = Util.getShortClassName(clazz);
			var bean:Object = getBean(beanName);
			if(bean is clazz){
				return bean;
			}else{
				throw new Error("Type mismatch error.required type="+clazz+" ,but bean type="+bean);
			}
		}
		/**
		 * beanNameを使用してbeanを検索し、beanインスタンスを返します。
		 */
		public function getBean(beanName:String):*{			
			_beanScope.init();
			return resolveBean(beanName);
		}
		private function resolveBean(beanName:String):Object{
			if(_initialized==false){
				initialize();
			}
			var resolvedBean:Object = _beanScope.findBeanByName(beanName);
			if(resolvedBean != null){
				return resolvedBean;
			}
			
			var context:BeanResolveContext = new BeanResolveContext();
			context.beanName = beanName;
			
			var desc:CSSStyleDeclaration = findStyleDeclaration(beanName);
			if(desc == null){
				throw new Error("CSSStyleDeclaration not found.selector="+context.beanName);
			}
			context.beanDesc = desc;
			
			var clazz:Class = Class(desc.getStyle(TYPE_CONFIG_NAME));
			if(clazz == null){
				throw new Error("[bean-type] property is not defined.beanName="+context.beanName);
			}
			context.beanClass = clazz;			
			context.bean = new clazz();
			
			initScope(context);
			resolveProperty(context);
			invokeInitMethod(context);
			logger.debug("beanName="+context.beanName+
				" class="+getQualifiedClassName(context.beanClass)+ " scope="+context.scope);
			return context.bean;
		}
		private function findStyleDeclaration(beanName:String):CSSStyleDeclaration{
			var desc:CSSStyleDeclaration = StyleManager.getStyleDeclaration(beanName);
			if(desc==null){
				desc = StyleManager.getStyleDeclaration("."+beanName);
			}
			return desc;
		}
		private function initScope(context:BeanResolveContext):void{
			var scope:String = context.beanDesc.getStyle(SCOPE_CONFIG_NAME);
			if(scope==null){
				scope = _defaultBeanScope;
			}
			checkScopeValue(scope);
			_beanScope.addBean(scope,context.beanName,context.bean);
			context.scope = scope;
		}
		private function resolveProperty(context:BeanResolveContext):void{
			var factory:Function = context.beanDesc.factory;
			if (factory != null){
	            factory.prototype = {};
				var bean:Object = context.bean;
	            var configMap:Object = new factory();
	            for(var name:String in configMap){
	            	if(isProperty(name)){
		            	var value:Object = configMap[name];
		            	bean[name] = evaluate(value);
		            }
	            }
	        }
		}
		private function isProperty(name:String):Boolean{
			for(var i:int = 0;i < CONFIG_NAME_LIST.length;i++){
				if(name==CONFIG_NAME_LIST[i]){
					return false;
				}				
			}
			return true;
		}
		private function evaluate(value:Object):Object{
			if(value is String){
				var valueString:String = value as String;
				if(BEAN_REF_PATTEN.test(valueString)){
					valueString = valueString.substring("${".length,valueString.length-1);
					return resolveBean(valueString);
				}
			}else if(value is Array){
				var valueArray:Array = value as Array;
				for(var i:int = 0;i < valueArray.length;i++){
					valueArray[i] = evaluate(valueArray[i]);
				}
				return valueArray;
			}
			return value;
		}
		private function invokeInitMethod(context:BeanResolveContext):void{
			var typeInfo:XML = _typeInfoCache.getTypeInfo(context.beanClass);
			var methodInfoList:XMLList = typeInfo.factory.method;
			var bean:Object = context.bean;
			
			//init namespace
			var initNamespaceMathodList:XMLList = methodInfoList.(attribute('uri') == init.uri);
			for each(var methodInfo:XML in initNamespaceMathodList){
				var methodArguments:Array = resolveArguments(methodInfo);
				bean.init::[methodInfo.@name].apply(bean,methodArguments);	
			}
			//css desc
			var initMethodDesc:Object = context.beanDesc.getStyle(INIT_METHOD_CONFIG_NAME);
			if(initMethodDesc != null){
				var initMethodNameList:Array = 
					(initMethodDesc is String) ? [initMethodDesc] : initMethodDesc as Array;
				for each(var methodName:String in initMethodNameList){
					var info:XMLList = methodInfoList.(@name==methodName);
					for each(var descMethodInfo:XML in info){
						var arguments:Array =resolveArguments(descMethodInfo);
						bean[descMethodInfo.@name].apply(bean,arguments);	
					}
				}
			}
		}
		private function resolveArguments(methodInfo:XML):Array{
			var params:XMLList = methodInfo.parameter;
			var argumentList:Array = [];
			for each(var param:XML in params){
				var paramType:String = param.@type;
				var paramBeanName:String = Util.getShortClassName(paramType);
				var paramBeanDesc:CSSStyleDeclaration =findStyleDeclaration(paramBeanName);
				if(paramBeanDesc==null){
					if(param.@optional=="true"){
						argumentList.push(null);
					}else{
						throw new ArgumentError("argument not found.argument type="+paramType);
					}
				}else{
					argumentList.push(resolveBean(paramBeanName));
				}
			}
			return argumentList
		}
		private function checkScopeValue(scope:String):void{			
			if(scope != BeanScope.SCOPE_SINGLETON && scope != BeanScope.SCOPE_PROTOTYPE){
				throw new Error("BeanScope ["+scope+"] not support.");
			}			
		}
	}
}

class BeanScope{
	public static const SCOPE_SINGLETON:String = "singleton";
	public static const SCOPE_PROTOTYPE:String = "prototype";

	private static var singletonScopeMap:Object = {};
	private var resolveScopeMap:Object;
	
	public function init():void{
		resolveScopeMap = {};
	}
	public function addBean(scope:String,beanName:String,bean:Object):void{
		if(scope == SCOPE_SINGLETON){
			singletonScopeMap[beanName] = bean;
		}else{
			resolveScopeMap[beanName] = bean;		
		}
	}
	
	public function findBeanByName(beanName:String):Object{
		var resolvedBean:Object = singletonScopeMap[beanName];
		if(resolvedBean == null){
			resolvedBean = resolveScopeMap[beanName];
		}
		return resolvedBean;
	}
}

import mx.styles.CSSStyleDeclaration;
class BeanResolveContext{
	public var bean:Object;
	public var beanClass:Class;
	public var beanName:String;
	public var beanDesc:CSSStyleDeclaration;
	public var resolveScopeMap:Object;
	public var scope:String;
}

import flash.utils.Dictionary;
import flash.utils.describeType;
class TypeInfoCache{
	private static const cache:Dictionary = new Dictionary(true);
	public function getTypeInfo(clazz:Class):XML{
		var info:XML = cache[clazz];
		if(info == null){
			info = describeType(clazz);
			cache[clazz] = info;
		}
		return info;
	}
}

import flash.utils.getQualifiedClassName;
class Util{
	/**
	 * クラスのショートネームを取得します。
	 * @param clazz Class or String
	 * @return クラスショートネーム
	 */
	public static function getShortClassName(clazz:*):String{
		var name:String = (clazz is String ) ? clazz as String : getQualifiedClassName(clazz);
		name = name.replace("::",".");
		var dot:int = name.lastIndexOf(".");
		if(dot!=-1){
			name = name.substring(dot+1,name.length);
		}
		return name;			
	}
}

